package com.echo.im.domain.user.service;


import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.temp.SaTempUtil;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.echo.im.client.IMClient;
import com.echo.im.common.contant.Constants;
import com.echo.im.common.exception.GlobalException;
import com.echo.im.client.model.IMPlatformMessage;
import com.echo.im.domain.message.model.UserLogoutMessage;
import com.echo.im.common.result.ResultEnum;
import com.echo.im.domain.user.vo.LoginVO;

import com.echo.im.domain.user.dto.LoginDTO;
import com.echo.im.domain.user.dto.ModifyPwdDTO;
import com.echo.im.domain.user.dto.RefreshTokenDTO;
import com.echo.im.domain.user.dto.RegisterDTO;
import com.echo.im.infrastructure.session.SessionContext;
import com.echo.im.infrastructure.session.UserSession;
import com.echo.im.repository.dao.entity.User;
import com.echo.im.repository.dao.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.Objects;

/**
 * 用户服务实现类
 * @author echo
 * @date 2024-08-16
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    private final PasswordEncoder passwordEncoder;
    @Value("${sa-token.refresh-token-timeout}")
    private Long refreshTokenTimeout ;
    private final IMClient imClient;

    /**
     * 用户登录
     * @param dto 登录数据传输对象，包含用户名、密码和终端信息
     * @return 登录成功后的用户信息和令牌
     * @throws GlobalException 如果用户不存在或密码错误等异常情况
     */
    @Override
    public LoginVO login(LoginDTO dto) {
        // 验证用户
        User user = validateUser(dto.getUserName(), dto.getPassword());

        // 构建用户会话
        UserSession session = buildUserSession(user, dto.getTerminal());
        // 用户登录
        StpUtil.login(user.getId(),dto.getTerminal().toString());

        // 设置用户会话信息
        StpUtil.getTokenSession().set(Constants.TOKEN_SESSION_NAME,session);

        // 构建并返回登录信息
        return buildLoginVO(session);
    }

    /**
     * 刷新令牌
     * @return 刷新后的用户信息和令牌
     * @throws GlobalException 如果刷新令牌无效或用户不存在
     */
    @Override
    public LoginVO refreshToken(RefreshTokenDTO dto) {
        String refreshToken = dto.getRefreshToken();
        Integer terminal=dto.getTerminal();
        // 从刷新令牌中解析用户ID
        Long userId=null;

        Object token = SaTempUtil.parseToken(refreshToken);
        if (token==null){
            throw new GlobalException(ResultEnum.NO_LOGIN);
        }

        userId= Long.parseLong(token.toString());

        // 校验用户ID是否有效
        if (Objects.isNull(userId)){
            throw new GlobalException(ResultEnum.NO_LOGIN);
        }

        // 验证用户是否存在
        User user = validateUserExistence(userId);

        // 构建用户会话
        UserSession session = buildUserSession(user, terminal);

        // 用户重新登录
        StpUtil.login(user.getId(),terminal);

        // 删除刷新令牌，保证只能刷新一次
        SaTempUtil.deleteToken(refreshToken);

        // 构建并返回登录信息
        return buildLoginVO(session);
    }

    /**
     * 用户登出
     */
    @Override
    public void logout() {
        UserSession session = SessionContext.getSession();
        // 注销 im-platform 观关联的用户
        StpUtil.logout();
        // 发送伪装消息至IM服务器，使用户下线

        // 发送指令至IM-engin，下线用户所有终端
        UserLogoutMessage userLogoutMessage = new UserLogoutMessage();
        userLogoutMessage.setUserId(session.getUserId());
        IMPlatformMessage<UserLogoutMessage> imPlatformMessage = new IMPlatformMessage<>();
        imPlatformMessage.setData(userLogoutMessage);
        imPlatformMessage.setRecvIds(Collections.singletonList(session.getUserId()));
        imPlatformMessage.setRecvTerminals(Collections.singletonList(session.getTerminal()));
        imClient.sendMessage(imPlatformMessage);
    }

    /**
     * 修改用户密码
     * @param dto 密码修改数据传输对象，包含旧密码和新密码
     * @throws GlobalException 如果旧密码错误
     */
    @Override
    public void modifyPassword(ModifyPwdDTO dto) {
        // 获取当前用户会话
        UserSession session = SessionContext.getSession();
        // 获取当前用户信息
        User user = this.getById(session.getUserId());
        // 校验旧密码是否正确
        if (!passwordEncoder.matches(dto.getOldPassword(), user.getPassword())) {
            throw new GlobalException(ResultEnum.OLD_PASSWORD_ERROR);
        }
        // 修改密码
        user.setPassword(passwordEncoder.encode(dto.getNewPassword()));
        this.updateById(user);

        // 注销所有设备
        StpUtil.logout(session.getUserId(),null);

        // 发送指令至IM-engin，下线用户所有终端
        UserLogoutMessage userLogoutMessage = new UserLogoutMessage();
        userLogoutMessage.setUserId(session.getUserId());
        IMPlatformMessage<UserLogoutMessage> imPlatformMessage = new IMPlatformMessage<>();
        imPlatformMessage.setData(userLogoutMessage);
        imPlatformMessage.setRecvIds(Collections.singletonList(session.getUserId()));
        imClient.sendMessage(imPlatformMessage);

        // 记录日志
        log.info("用户修改密码，用户id:{},用户名:{},昵称:{}", user.getId(), user.getUserName(), user.getNickName());
    }

    /**
     * 用户注册
     * @param dto 注册数据传输对象，包含用户信息
     * @throws GlobalException 如果用户名已存在
     */
    @Override
    public void register(RegisterDTO dto) {
        // 检查用户名是否已存在
        User user = baseMapper.selectUserByUserName(dto.getUserName());
        if (!Objects.isNull(user)) {
            throw new GlobalException(ResultEnum.USERNAME_ALREADY_REGISTER);
        }
        // 复制DTO属性到User对象
        user = BeanUtil.copyProperties(dto, User.class);
        // 加密密码
        user.setPassword(passwordEncoder.encode(user.getPassword()));
        // 插入用户数据
        baseMapper.insert(user);
        // 记录日志
        log.info("注册用户，用户id:{},用户名:{},昵称:{}", user.getId(), dto.getUserName(), dto.getNickName());
    }

    /**
     * 验证用户
     * @param userName 用户名
     * @param password 密码
     * @return 用户对象
     * @throws GlobalException 如果用户不存在或密码错误
     */
    private User validateUser(String userName, String password) {
        // 根据用户名查找用户
        User user = baseMapper.selectUserByUserName(userName);
        if (Objects.isNull(user)) {
            throw new GlobalException(ResultEnum.USER_NOT_EXIST);
        }

        // 校验用户是否被禁用
        if (user.getIsBanned()) {
            throw new GlobalException(ResultEnum.USER_BANNED);
        }

        // 校验密码是否正确
        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new GlobalException(ResultEnum.PASSWOR_ERROR);
        }

        return user;
    }

    /**
     * 验证用户是否存在
     * @param userId 用户ID
     * @return 用户对象
     * @throws GlobalException 如果用户不存在或被禁用
     */
    private User validateUserExistence(Long userId) {
        // 根据ID查找用户
        User user = this.getById(userId);
        if (Objects.isNull(user)) {
            throw new GlobalException(ResultEnum.USER_NOT_EXIST);
        }

        // 校验用户是否被禁用
        if (user.getIsBanned()) {
            throw new GlobalException(ResultEnum.USER_BANNED);
        }

        return user;
    }

    /**
     * 构建用户会话信息
     * @param user 用户对象
     * @param terminal 终端类型
     * @return 用户会话对象
     */
    private UserSession buildUserSession(User user, Integer terminal) {
        UserSession session = new UserSession();
        session.setUserId(user.getId());
        session.setUserName(user.getUserName());
        session.setNickName(user.getNickName());
        session.setTerminal(terminal);
        return session;
    }

    /**
     * 构建登录返回信息
     * @param session 用户会话信息
     * @return 登录信息对象，包含访问令牌和刷新令牌等
     */
    private LoginVO buildLoginVO(UserSession session) {
        SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
        String accessToken = tokenInfo.getTokenValue();
        String refreshToken = SaTempUtil.createToken(session.getUserId(), refreshTokenTimeout);

        LoginVO vo = new LoginVO();
        vo.setAccessToken(accessToken);
        vo.setAccessTokenExpiresIn(tokenInfo.getTokenTimeout());
        vo.setRefreshToken(refreshToken);
        vo.setRefreshTokenExpiresIn(refreshTokenTimeout);
        return vo;
    }

}
